/*
 * Copyright (c) 2005, 2014 vacoor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package org.vacoor.nothing.misc.dts.oo.comp;

import com.sun.star.bridge.XBridge;
import com.sun.star.bridge.XBridgeFactory;
import com.sun.star.comp.helper.Bootstrap;
import com.sun.star.connection.NoConnectException;
import com.sun.star.connection.XConnection;
import com.sun.star.connection.XConnector;
import com.sun.star.frame.XComponentLoader;
import com.sun.star.lang.EventObject;
import com.sun.star.lang.XComponent;
import com.sun.star.lang.XEventListener;
import com.sun.star.lang.XMultiComponentFactory;
import com.sun.star.uno.XComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.vacoor.nothing.common.misc.util.Platform;
import org.vacoor.nothing.common.util.Threads;
import org.vacoor.nothing.misc.dts.oo.util.UnoUtils;

import java.io.File;
import java.util.UUID;

/**
 * UNO Bridge
 * UNO 支持两种连接方式: Pipe, Socket
 * 1. Pipe 方式: Server-Client 处于同一个台机器, 共享内存, 相对于 Socket 效率更高
 * 2. Socket 方式:
 * <p />
 * 每个 UNO Bridge 是线程安全的, 但是
 * -- 对于 Pipe 方式是共享内存, 因此当有多个 UNO Pipe Bridge 连接同一 Server 进行并行处理则是不安全的, 也就意味着当使用 Pipe
 * 方式时只应该建立一个 Bridge (Socket 不存在该问题)
 * -- 对于 Socket 方式, 当有多个 UNO Socket Brige 连接同一 Server 时, 性能开销远远大于使用一个 Bridge 进行并行处理
 * <p/>
 * 建议: 一个 Server 只建立一个 UNO Bridge, 并严格限制每个 Bridge 的并行数量(每个 Bridge 性能损耗 与 并行任务数为抛物线关系)
 * 警告: Bridge 连接不是稳定的，因此建议在使用 bridge 先通过 isConnected 判断
 * <p/>
 * UnoUrl u = UnoUrl.parseUnoUrl(unoUrl + ";urp;");
 * 简单包装下 UnoBridge
 * "uno:pipe,name=123;urp;StarOffice.ComponentContext";
 * "uno:socket,host=192.168.125.130,port=9999:urp;";
 * StarOffice.ServiceManager
 *
 * 参考《RedOffice 高级开发支持》
 * <p/>
 * 附所需 jar:
 * 第一种方式: maven repositories
 * 第二种方式:
 * 1. /usr/share/libreoffice/ure-link/share/java/juh.jar,
 * 2. /usr/share/libreoffice/ure-link/share/java/jurt.jar,
 * 3. /usr/share/libreoffice/ure-link/share/java/ridl.jar
 * 4. /usr/share/libreoffice/program/classes/unoil.jar
 * 对于 4,
 * 需要安装 SDK: Ubuntu libreoffice SDK sudo apt-get install libreoffice-dev
 * MAC OS X 下 下载 SDK 运行 setsdkenvunix over, 到 Libreoffice.app/Content 下查找以上目录
 *
 * @author vacoor
 */
@SuppressWarnings({"unused"})
public class UnoBridge {
    private static final Logger LOG = LoggerFactory.getLogger(UnoBridge.class);

    private static final String UNO_URL_RESOLVER = "com.sun.star.bridge.UnoUrlResolver";
    private static final String BRIDGE_FACTORY = "com.sun.star.bridge.BridgeFactory";
    private static final String CONNECTOR = "com.sun.star.connection.Connector";
    private static final String REMOTE_COMP_CONTEXT = "StarOffice.ComponentContext";
    private static final String REMOTE_SMGR_NAME = "StarOffice.ServiceManager";
    private static final String REMOTE_DESKTOP = "com.sun.star.frame.Desktop";

    private static final String BRIDGE_PREFIX = "Uno-Bridge-";
    private static final Object BRIDGE_MONITOR = new Object();

    private static final int RETRY_COUNT = 20;

    private final String url;
    private final String bridgeType;
    private final XEventListener xBridgeMonitor = new XEventListener() {
        @Override
        public void disposing(EventObject paramEventObject) {
            disconnect(); // bridge 异常/手动断开时调用断开方法, 这样写好像不太好
        }
    };
    private XBridge xBridge;

    public UnoBridge(String url, String bridgeType) {
        // 对于 Socket 默认会有 200ms 延迟
        if (url.toLowerCase().startsWith("socket")) {
            url += ",tcpNoDelay=1";
        }

        this.url = url;
        this.bridgeType = bridgeType;
    }

    public static UnoBridge of(String url, String bridgeType) {
        return new UnoBridge(url, bridgeType);
    }

    /**
     * @throws Exception
     */
    public UnoBridge connect() throws Exception {
        if (isConnected()) {
            return this;
        }

        loadJPipeNativeLibraryIfNeed(url, null);
        XComponentContext xLocalCompCtx = Bootstrap.createInitialComponentContext(null);

        // 这里不同工厂实例其实 bridge 是共存的, 所以就不处理了
        XBridgeFactory xBridgeFactory = UnoUtils.$(xLocalCompCtx, XBridgeFactory.class, BRIDGE_FACTORY);

        String xBridgeName = BRIDGE_PREFIX + url + ";" + bridgeType;

        /**
         * 对于 PIPE 不应该存在多个 bridge, 但是 socket 方式没有问题
         */
        if (url.startsWith("socket")) {
            xBridgeName += "-" + UUID.randomUUID();
        }

        synchronized (BRIDGE_MONITOR) {
            xBridge = xBridgeFactory.getBridge(xBridgeName);

            if (xBridge == null) {
                for (int i = 0; i < RETRY_COUNT; i++) {
                    try {
                        XConnector xConnector = UnoUtils.$(xLocalCompCtx, XConnector.class, CONNECTOR);
                        XConnection xConnection = xConnector.connect(url);
                        /*-
                         * 这里, 应该是通过名称来判断, 即使工厂不是同一个实例, 但是如果 bridge 名称相同
                         * 非并发会抛出 BridgeExistsException
                         * 并发情况下居然会返回同一个 Bridge 对象
                         * 奇怪, 有空看下
                         * createBridge 存在并发问题, 非并发情况下如果 bridge 存在会抛出 BridgeExistsException
                         */
                        xBridge = xBridgeFactory.createBridge(xBridgeName, bridgeType, xConnection, null);
                        LOG.info("UnoBridge connect successful, bridgeName:{}, url: {}, bridgeType: {}", xBridgeName, url, bridgeType);
                        XComponent xBridgeComponent = UnoUtils.$(XComponent.class, xBridge);
                        xBridgeComponent.addEventListener(xBridgeMonitor);
                        break;
                    } catch (NoConnectException nce) {
                        if (i == RETRY_COUNT - 1) {
                            throw new RuntimeException(String.format("UnoBridge connect failed, url: %s, bridgeType: %s", url, bridgeType), nce);
                        }
                        LOG.warn("UnoBridge connect failed, retry please, url: {}, bridgeType: {}", url, bridgeType);
                        Threads.sleep(3 * 1000);
                    }
                }
            } else {
                LOG.warn("PIPE 不应该有多个 bridge, 使用已经存在的 UnoBridge, bridgeName: {}, url: {}, bridgeType: {}", xBridgeName, url, bridgeType);
            }

            //
            // TODO new URLClassLoader("") 这样就可以加载了哈
            /*
            //
            XTextDocument text = $(XTextDocument.class, createRemoteComponentLoader().loadComponentFromURL("private:factory/swriter", "_blank", 0, new PropertyValue[0]));
            // 获取文档引用
            XText xText = text.getText();
            // 获取文档属性
            XPropertySet xDocProps = $(XPropertySet.class, xText);
            // 建立光标
            XTextCursor xTextCursor = xText.createTextCursor();
            //
            XSentenceCursor xSentenceCursor = $(XSentenceCursor.class, xTextCursor);
            XWordCursor xWordCursor = $(XWordCursor.class, xTextCursor);
            //
            XPropertySet xCursorProps = $(XPropertySet.class, xTextCursor);
            xCursorProps.setPropertyValue("CharFontName", "宋体");
            xCursorProps.setPropertyValue("CharWeight", new Float(FontWeight.BOLD));
            xCursorProps.setPropertyValue("CharHeight", new Float(10.5));
            // 居中显示
            xCursorProps.setPropertyValue("ParaAdjust", com.sun.star.style.ParagraphAdjust.CENTER);
            xText.insertString(xSentenceCursor, "Hello World", true);
            */

            // $(XSpreadsheetDocument.class, createRemoteComponentLoader().loadComponentFromURL("private:factory/scalc", "_blank", 0, new PropertyValue[0]))
            // $(XExportFilter.class, createRemoteComponentLoader().loadComponentFromURL("private:factory/scalc", "_blank", 0, new PropertyValue[0]))
            // $(XPresentationPage.class, createRemoteComponentLoader().loadComponentFromURL("private:factory/simpress", "_blank", 0, new PropertyValue[0]));
            // XComponent xComponent = createRemoteComponentLoader().loadComponentFromURL("private:factory/simpress", "_blank", 0, new PropertyValue[0]);

            return this;
        }
    }

    public boolean isConnected() {
        return xBridge != null;
    }

    /**
     * disconnect for bridge
     */
    public void disconnect() {
        if (!isConnected()) {
            return;
        }
        LOG.warn("UnoBridge disconnected, url: {}, bridgeType: {}", url, bridgeType);
        try {
            XComponent xBridgeComponent = UnoUtils.$(XComponent.class, xBridge);
            if (xBridgeComponent != null) {
                xBridgeComponent.removeEventListener(xBridgeMonitor);
                xBridgeComponent.dispose();
            }
        } catch (Exception ignore) {
            // ignore
        } finally {
            xBridge = null;
        }
    }

    /**
     * get current bridge name
     */
    public String getBridgeName() {
        return xBridge != null ? xBridge.getName() : null;
    }

    /**
     * create decode component loader use current uno bridge
     *
     * @return null or component
     * @throws Exception
     */
    public XComponentLoader createRemoteComponentLoader() throws Exception {
        return UnoUtils.$(getRemoteComponentContext(), XComponentLoader.class, REMOTE_DESKTOP);
    }

    /**
     * 返回 bridge 中 XComponentContext
     * get remote component context locator in current object wrapper's uno bridge
     */
    public XComponentContext getRemoteComponentContext() {
        return getRemoteComponent(XComponentContext.class, REMOTE_COMP_CONTEXT);
    }

    /**
     * 获取 bridge 中 ServiceManager
     * get remote service manager locator in current bridge
     */
    public XMultiComponentFactory getRemoteServiceManager() {
        /*-
         * 该方式为获取 "/singleton/com.sun.star.lang.theServiceManager" or "StartOffice.ServiceManager"
         * 命名单例对象的快捷操作
         */
        return getRemoteComponentContext().getServiceManager();
    }

    /**
     * export singleton object by current bridge
     *
     * @param xCmpType Uno component type
     * @param name     Uno component service name
     */
    public <T> T getRemoteComponent(Class<T> xCmpType, String name) {
        if (xBridge == null) {
            throw new IllegalStateException("UnoBridge is not connection, must be call connect method first");
        }
        return UnoUtils.$(xCmpType, xBridge.getInstance(name));
    }

    /**
     * ensure JPipe native lib loaded when connection type is PIPE
     *
     * @param ooHome openoffice/libreoffice home, if it's null will use {@link org.vacoor.nothing.misc.dts.oo.util.UnoUtils#findOOHome()}
     * @throws UnsatisfiedLinkError
     */
    private static void loadJPipeNativeLibraryIfNeed(String url, final String ooHome) throws UnsatisfiedLinkError {
        if (nativeLoaded || !url.toLowerCase().startsWith("pipe")) {
            return;
        }

        /*-
         * sal native library will autoload when load jpipe
         * sal name:
         *     "libuno_sal.dylib.3" in mac osx,
         *     "sal.dll" in windows
         */
        final String libName = "jpipe";
        synchronized (UnoBridge.class) {
            if (nativeLoaded) {
                return;
            }

            try {
                // first, try load from java.library.path
                System.loadLibrary(libName);
                LOG.debug("loaded native library '{}' from java.library.path", libName);
                nativeLoaded = true;
                return;
            } catch (UnsatisfiedLinkError ignore) {
                // ignore
            }

            // TODO jvm 只在第一次加载 nativeLib 时根据 java.library.path 初始化一次查找路径, 这里重新查找一次
            /**
             系统缓存了java.library.path的值，并且一直都会是第一次加载时的值。

             原因和ClassLoader的实现有关系，
             ClassLoader.loadLibrary() method:
             Java代码
             if (sys_paths == null) {
             usr_paths = initializePath("java.library.path");
             sys_paths = initializePath("sun.boot.library.path");
             }
             */
            String nativeDir = ooHome != null ? ooHome : UnoUtils.findOOHome();
            if (nativeDir != null) {
                nativeDir += !nativeDir.endsWith("/") ? "/" : "";
                nativeDir += (Platform.isWindows() ? "URE/bin" : "ure-link/lib");
            }

            try {
                // next, try load use absolute path
                String path = new File(nativeDir, libName).getAbsolutePath();
                System.load(path);
                LOG.debug("loaded native library: {}", path);
                nativeLoaded = true;
                return;
            } catch (UnsatisfiedLinkError ignore) {
                // ignore
            }

            try {
                // end, try load use absolute path and map library name
                // mac mapLibraryName("jpipe") --> libjpipe.dylib, real libjpipe.jnilib
                String path = new File(nativeDir, System.mapLibraryName(libName).replaceAll("dylib$", "jnilib")).getAbsolutePath();
                System.load(path);
                LOG.debug("loaded native library: {}", path);
                nativeLoaded = true;
            } catch (UnsatisfiedLinkError e) {
                UnsatisfiedLinkError error = new UnsatisfiedLinkError("load jpipe native library failed, please install OpenOffice/LibreOffice, and setting OO_HOME or configuration application");
                error.initCause(e);
                throw error;
            }
        }
    }

    private static volatile boolean nativeLoaded;
}
